信号中断系统调用:EINTR 与 SA_RESTART
注册了信号处理函数的程序,经常在处理函数执行完之后出问题:阻塞在 read()、accept() 上的系统调用莫名返回 -1,errno 是 EINTR;没料到这一层的程序轻则报错退出,重则状态错乱、出现堆栈异常。本文写给写信号处理逻辑、维护长驻服务或网络服务的 C/C++ 开发者,读完可以弄清 EINTR 的产生机制、两种标准应对方式,以及 SA_RESTART 标志到底能覆盖哪些系统调用——它在 Linux 上并不是万能的。
信号机制本身(产生、阻塞、捕捉的原理)见 Linux 信号机制:产生、阻塞与捕捉,本文专注「信号处理函数与系统调用的交互」这一个专题。
问题现场
典型症状:程序捕获信号并执行信号处理函数,返回原执行流后,原本阻塞的系统调用不再继续等待,而是以失败告终——返回 -1,errno 被置为 EINTR(Interrupted system call)。上层代码若只按普通 I/O 错误处理,就可能异常退出或进入未定义状态。
中断是如何发生的
程序阻塞在系统调用中(如 read()、write()、accept())时收到信号,完整时序是:
- 系统调用被中断:阻塞状态被立即打断;
- 执行信号处理函数:内核转去执行已注册的处理函数;
- 系统调用返回失败:处理函数完成后,被中断的系统调用默认返回
-1,不再自动继续; - 错误码设置:
errno被设置为EINTR; - 程序异常退出:若调用方未处理该错误,程序可能异常终止或出现堆栈错误。
这是内核的默认行为,不是 bug——它把「要不要重试」的决定权交给了应用程序。
易受影响的系统调用
未设置 SA_RESTART 时,以下系统调用被信号中断后会返回 -1 并置 errno = EINTR:
read()/write():文件、socket 读写accept()/connect():网络连接nanosleep():休眠(返回-1/EINTR,剩余时间通过rem参数带出)wait()/waitpid():进程等待select()/poll()/epoll_wait():I/O 多路复用
注意一个特例:sleep(3) 被中断时不报错,而是返回剩余未休眠的秒数(Linux 上基于 nanosleep 实现,见 man 3 sleep)。
方案一:设置 SA_RESTART 标志(推荐)
注册信号处理函数时用 sigaction,把 sa_flags 设置为 SA_RESTART,被中断的系统调用会在处理函数返回后自动重新执行,调用方感知不到中断:
#include <signal.h>
#include <unistd.h>
#include <iostream>
void signal_handler(int signum) {
std::cout << "Caught signal " << signum << std::endl;
// 处理信号...
}
int main() {
struct sigaction sa;
sa.sa_handler = signal_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART; // 关键:被中断的系统调用自动重启
if (sigaction(SIGINT, &sa, nullptr) == -1) {
perror("sigaction");
return 1;
}
char buffer[1024];
ssize_t n = read(STDIN_FILENO, buffer, sizeof(buffer));
if (n == -1) {
perror("read");
} else {
std::cout << "Read " << n << " bytes" << std::endl;
}
return 0;
} 方案二:手动检查 EINTR 并重试
不能用 SA_RESTART(或需要更精细控制)时,手动检查 errno 并重试是通用做法,适合封装成基础库函数:
#include <unistd.h>
#include <cerrno>
ssize_t safe_read(int fd, void* buf, size_t count) {
ssize_t n;
while ((n = read(fd, buf, count)) == -1) {
if (errno == EINTR) {
continue; // 被信号中断,重试
} else {
return -1; // 其他错误,返回失败
}
}
return n;
} SA_RESTART 的边界:哪些调用不会自动重启
SA_RESTART 并非对全部系统调用生效。依据 man 7 signal,Linux 上以下调用即使设置了 SA_RESTART 也不会自动重启,被中断时始终返回 EINTR:
- I/O 多路复用:
select()/pselect()/poll()/ppoll()/epoll_wait()/epoll_pwait()——不是「行为依赖于实现」,Linux 上明确不重启 - 休眠:
nanosleep()/clock_nanosleep()/usleep(),通过剩余时间参数返回进度 - 等待信号类:
pause()/sigsuspend()/sigtimedwait()/sigwaitinfo() - System V IPC:
msgrcv()/msgsnd()/semop()/semtimedop() - 设置了超时的 socket 调用:通过
setsockopt()设置了SO_RCVTIMEO/SO_SNDTIMEO的recv()/send()/accept()/connect()等
相对应地,以下调用设置 SA_RESTART 后会自动重启:
- 慢速设备(终端、管道、socket)上的
read()/readv()/write()/writev()/ioctl() wait()/waitpid()/wait3()/wait4()(注意waitid()例外,不重启)- 未设置超时的 socket 调用(
accept()/connect()/recv()/send()等) flock()、fcntl()的F_SETLKW/F_OFD_SETLKW、POSIX 消息队列、sem_wait()、getrandom()等
最佳实践
- 默认使用
SA_RESTART:多数信号处理场景下,设置此标志可以免除一层不必要的错误处理; - 关键路径手动检查 EINTR:涉及
poll/epoll、nanosleep等「SA_RESTART 管不到」的调用,或需要精细控制重试语义时,手动检查EINTR并决定是否重试; - 信号处理函数保持简洁:只做置标志位等最小动作,避免在处理函数中执行复杂操作,减少与主流程的竞态窗口。
要点
- 信号处理函数返回后,被中断的阻塞系统调用默认失败返回,
errno = EINTR——不是崩溃,是需要处理的标准行为 SA_RESTART让read/write/accept/waitpid等自动重启,但select/poll/epoll_wait、nanosleep、SysV IPC、设超时的 socket 调用无论如何都不重启,必须手动处理EINTRsleep(3)特例:被中断不报错,返回剩余秒数- 两种方案可以并存:全局
SA_RESTART兜底,关键调用再包一层EINTR重试